# -*- coding: utf-8 -*-

import logging
from itertools import chain

import random

from common.cache.redis_cache import get_total_bet, get_system_profit
from common.lottery import LOTTERY_TYPE, DIY_LOTTERY
from common.lottery.cyclical import ORDER_STATUS
from common.lottery.cyclical.abstract.activity import get_latest_unpublished_term, fullfill_result
from common.lottery.cyclical.abstract.order import WinOrderApplyer
from common.lottery.cyclical.model import ORDER_LOGIC
from common.lottery.cyclical.model import ORDER_MODEL
from common.preset.db.reward import check_reward
from common.timer import TIMER_EVENT_TYPE, TimerEvent
from common.utils import tz
from common.utils.decorator import sql_wrapper
from common.preset.db.profit_rate import get_profit_rate
from common.utils import track_logging

_LOGGER = track_logging.getLogger(__name__)

MAX_ADJUST_COUNT = 50
MIN_ADJUST_BET = 10000
# MIN_PROFIT_RATE = 0.05
# MAX_PROFIT_RATE = 0.2


NEED_DOT = {
    LOTTERY_TYPE.FF_SSC: False,
    LOTTERY_TYPE.FF_11X5: True,
    LOTTERY_TYPE.FF_KS: False,
    LOTTERY_TYPE.FF_PK10: True,
}

UNIQUE = {
    LOTTERY_TYPE.FF_SSC: False,
    LOTTERY_TYPE.FF_11X5: True,
    LOTTERY_TYPE.FF_KS: False,
    LOTTERY_TYPE.FF_PK10: True,
}

MIN_DIGIT_CONFIG = {
    LOTTERY_TYPE.FF_SSC: 0,
    LOTTERY_TYPE.FF_11X5: 1,
    LOTTERY_TYPE.FF_KS: 1,
    LOTTERY_TYPE.FF_PK10: 1,
}

MAX_DIGIT_CONFIG = {
    LOTTERY_TYPE.FF_SSC: 9,
    LOTTERY_TYPE.FF_11X5: 11,
    LOTTERY_TYPE.FF_KS: 6,
    LOTTERY_TYPE.FF_PK10: 10,
}

NUMBER_LENGTH_CONFIG = {
    LOTTERY_TYPE.FF_SSC: 5,
    LOTTERY_TYPE.FF_11X5: 5,
    LOTTERY_TYPE.FF_KS: 3,
    LOTTERY_TYPE.FF_PK10: 10,
}


def get_daily_pool_info(activity_type):
    return float(get_system_profit(activity_type)), float(get_total_bet(activity_type))


def calc_win(activity_type, number, orders):
    logic = ORDER_LOGIC[activity_type]
    funcs = logic.generate_win_func(number)

    result = []
    for order in orders:
        win_price = logic.calc_win_price(funcs, order['number'], order['bet_type'], order['times'], order['unit'])
        hits = check_reward(activity_type, order['term'], order['bet_type'])
        bonus, _ = WinOrderApplyer.calc_bonus(win_price, hits)
        result.append({
            'user_id': order['user_id'],
            'number': order['number'],
            'bet_amount': float(order['price']),
            'win_amount': float(win_price) + float(bonus),
        })

    return result


def get_normal_choice(min_digit, max_digit, size, replace):
    if replace:
        list_num = [random.randint(min_digit, max_digit) for _ in range(size)]
    else:
        list_num = random.sample(range(min_digit, max_digit + 1), size)
    return list_num


def normal_random(lottery_type, replace=None):
    min_digit = MIN_DIGIT_CONFIG.get(lottery_type)
    max_digit = MAX_DIGIT_CONFIG.get(lottery_type)
    number_length = NUMBER_LENGTH_CONFIG.get(lottery_type)
    need_dot = NEED_DOT.get(lottery_type)
    if replace is None:
        replace = not UNIQUE.get(lottery_type)

    list_num = get_normal_choice(min_digit, max_digit, number_length, replace=replace)

    if need_dot:
        str_num = ','.join(str(x) for x in list_num)
    else:
        str_num = ''.join(str(x) for x in list_num)
    return str_num


def get_probability_choice(digits_weight, size, replace=True):
    key_pool = []
    for key, value in digits_weight.iteritems():
        key_pool += [key] * int(round(value, 5) * 100000)
    ret = []
    for _ in range(0, size):
        value = random.choice(key_pool)
        if not replace:
            key_pool = filter(lambda a: a != value, key_pool)
        ret.append(value)
    return ret


def probability_random(digits_weight, lottery_type, replace=None):
    number_length = NUMBER_LENGTH_CONFIG.get(lottery_type)
    need_dot = NEED_DOT.get(lottery_type)
    if replace is None:
        replace = not UNIQUE.get(lottery_type)

    list_num = get_probability_choice(digits_weight, number_length, replace=replace)

    if need_dot:
        str_num = ','.join(str(x) for x in list_num)
    else:
        str_num = ''.join(str(x) for x in list_num)
    return str_num


def probability_random_wrap(digits_weight, lottery_type):
    if lottery_type == LOTTERY_TYPE.FF_SSC:
        number = probability_random(digits_weight, lottery_type)
        rand = random.randint(1, 99)
        if len(set(number)) == 4 and rand >= 50:
            _LOGGER.warn('probability_random, change to replace from 4:%s', number)
            number = probability_random(digits_weight, lottery_type, replace=True)
        elif len(set(number)) < 4 and rand >= 10:
            _LOGGER.warn('probability_random, change to replace from <4:%s', number)
            number = probability_random(digits_weight, lottery_type, replace=True)
        return number
    else:
        return probability_random(digits_weight, lottery_type)


def get_all_orders(activity_type, term):
    order_table = ORDER_MODEL[activity_type]

    order_query = order_table.query. \
        filter(order_table.term == term). \
        filter(order_table.status == ORDER_STATUS.READY).all()

    orders = []

    for order in order_query:
        order_dict = {
            'id': order.id,
            'user_id': order.user_id,
            'term': order.term,
            'number': order.number,
            'price': float(order.price),
            'bet_type': order.bet_type,
            'times': order.times,
            'unit': order.unit
        }
        orders.append(order_dict)

    return orders


def _generate_default_weight(min_digit, max_digit):
    weights = {}
    for i in xrange(min_digit, max_digit + 1):
        weights[i] = 0.01
    return weights


def _merge_digits_weight(dict1, dict2):
    dict3 = {}
    for k, v in chain(dict1.items(), dict2.items()):
        if k in dict3:
            dict3[k] += v
        else:
            dict3[k] = v
    return dict3


def _normalize_weight(weights):
    factor = 1.0 / sum(weights.values())
    normalized_weight = {}
    for k in weights:
        normalized_weight[k] = weights[k] * factor
    return normalized_weight


def user_win_digits_weight(orders, lottery_type):
    min_digit = MIN_DIGIT_CONFIG.get(lottery_type)
    max_digit = MAX_DIGIT_CONFIG.get(lottery_type)
    digits_weight = _generate_default_weight(min_digit, max_digit)
    for order in orders:
        number = order.get('number')
        digits = [int(x) for x in number if x.isdigit()]
        weight = _generate_default_weight(min_digit, max_digit)
        for digit in digits:
            if digit in weight:
                weight[digit] += order.get('price')
        digits_weight = _merge_digits_weight(digits_weight, weight)
    return _normalize_weight(digits_weight)


def user_lose_digits_weight(orders, lottery_type):
    min_digit = MIN_DIGIT_CONFIG.get(lottery_type)
    max_digit = MAX_DIGIT_CONFIG.get(lottery_type)
    digits_weight = _generate_default_weight(min_digit, max_digit)
    for order in orders:
        number = order.get('number')
        digits = [int(x) for x in number if x.isdigit()]
        weight = _generate_default_weight(min_digit, max_digit)
        for digit in digits:
            if digit in weight:
                weight[digit] += order.get('price')
        digits_weight = _merge_digits_weight(digits_weight, weight)
    reversed_weight = {}
    for k, v in _normalize_weight(digits_weight).iteritems():
        reversed_weight[k] = 1 - v
    return _normalize_weight(reversed_weight)


def choose_user_win_number(candidate_dict, max_profit_rate):
    update_dict = {}
    update_dict = dict([(k, candidate_dict[k]) for k in candidate_dict if candidate_dict[k] > 0])
    if len(update_dict) == 0:
        update_dict = candidate_dict
    number, _ = min(update_dict.items(), key=lambda (_, v): abs(v - max_profit_rate))
    return number


def choose_user_lose_number(candidate_dict, min_profit_rate):
    update_dict = {}
    update_dict = dict([(k, candidate_dict[k]) for k in candidate_dict if candidate_dict[k] > 0])
    if len(update_dict) == 0:
        update_dict = candidate_dict
        _LOGGER.warn('choose_user_lose_number, ensure profit fail')
    number, _ = min(update_dict.items(), key=lambda (_, v): abs(v - min_profit_rate))
    return number


def need_adjust_number(lottery_type, new_win, new_bet_count, ignore_apply_rate=False):
    _LOGGER.info("need_adjust_number {} {} {} {}".format(lottery_type, new_win, new_bet_count, ignore_apply_rate))
    preset = get_profit_rate(activity_type=lottery_type)

    new_profit_rate = new_win / new_bet_count

    if new_win > preset.preset_max_loss and new_bet_count < preset.preset_amount:
        _LOGGER.info("need_adjust_number1")
        return False
    if preset.preset_min_profit_rate <= new_profit_rate <= preset.preset_max_profit_rate:
        _LOGGER.info("need_adjust_number1")
        return False

    if new_profit_rate < preset.preset_min_profit_rate and not ignore_apply_rate:
        if random.uniform(0, 1) >= preset.preset_win_rate:
            _LOGGER.info("need_adjust_number3")
            return False

    if new_profit_rate > preset.preset_max_profit_rate and not ignore_apply_rate:
        if random.uniform(0, 1) >= preset.preset_loss_rate:
            _LOGGER.info("need_adjust_number4")
            return False

    return True


def generate_and_fill_diy(lottery_type):
    if lottery_type not in DIY_LOTTERY:
        _LOGGER.error(u'lottery_type <%s> is not DIY lottery, should not be call here' % lottery_type)
        return

    latest_unpublished_term = get_latest_unpublished_term(lottery_type)
    if not latest_unpublished_term:
        _LOGGER.debug(u'no unpublished term for %s' % DIY_LOTTERY.get(lottery_type))
        return

    term = latest_unpublished_term.term
    tag = '#%s%s#' % (DIY_LOTTERY.get(lottery_type), term)
    system_win, total_bet = get_daily_pool_info(lottery_type)
    current_profit_rate = 100 * system_win / float(total_bet) if total_bet else 0
    _LOGGER.info('%s current system_win: %s, bet: %s, profit_rate: %s%%' %
                 (tag, system_win, total_bet, round(current_profit_rate, 3)))

    orders = get_all_orders(lottery_type, term)
    bet_this_term = sum(x['price'] for x in orders)
    final_number = None
    preset = get_profit_rate(activity_type=lottery_type)
    if not preset:
        _LOGGER.info("generate_and_fill_diy but not preset {} ".format(lottery_type))

    if bet_this_term > 0 and preset:

        number = normal_random(lottery_type)
        result = calc_win(lottery_type, number, orders)
        win_this_term = sum(x['win_amount'] for x in result)

        new_win = system_win + bet_this_term - win_this_term
        new_bet_count = total_bet + bet_this_term
        new_profit_rate = new_win / new_bet_count

        _LOGGER.info(u'%s %s -> term_bet: %s, term_win: %s, rate: %s%%' %
                     (tag, number, bet_this_term, win_this_term, round(new_profit_rate * 100, 3)))

        if new_profit_rate > preset.preset_max_profit_rate and need_adjust_number(lottery_type, new_win, new_bet_count):
            _LOGGER.info(u'%s profit_rate > %s, need to make user win' % (tag, preset.preset_max_profit_rate))
            try_count = 0
            candidate_dict = {}
            while try_count < MAX_ADJUST_COUNT:
                number = normal_random(lottery_type)
                result = calc_win(lottery_type, number, orders)
                win_this_term = sum(x['win_amount'] for x in result)

                new_win = system_win + bet_this_term - win_this_term
                new_bet_count = total_bet + bet_this_term
                new_profit_rate = new_win / new_bet_count

                candidate_dict.update({
                    number: new_profit_rate
                })
                try_count += 1
                _LOGGER.info(u'%s %s -> term_bet: %s, term_win: %s, rate: %s%%, try count: %s' %
                             (tag, number, bet_this_term, win_this_term, round(new_profit_rate * 100, 3), try_count))
                if not need_adjust_number(lottery_type, new_win, new_bet_count, ignore_apply_rate=True):
                    final_number = number
                    _LOGGER.info(u'%s adjust succ, update number %s' % (tag, final_number))
                    break
            if not final_number:
                final_number = choose_user_win_number(candidate_dict, preset.preset_max_profit_rate)
                _LOGGER.info(u'%s adjust fail, choose_user_win_number %s' % (tag, final_number))
        elif new_profit_rate < preset.preset_min_profit_rate and need_adjust_number(lottery_type, new_win,
                                                                                    new_bet_count):
            _LOGGER.info(u'%s profit_rate < %s, need to make user lose' % (tag, preset.preset_min_profit_rate))
            try_count = 0
            digits_weight = user_lose_digits_weight(orders, lottery_type)
            candidate_dict = {}
            _LOGGER.info(u'%s digits_weight: %s' % (tag, digits_weight))
            while try_count < MAX_ADJUST_COUNT:
                number = probability_random_wrap(digits_weight, lottery_type)
                result = calc_win(lottery_type, number, orders)
                win_this_term = sum(x['win_amount'] for x in result)
                new_profit_rate = (system_win + bet_this_term - win_this_term) / (total_bet + bet_this_term)
                candidate_dict.update({
                    number: new_profit_rate
                })
                try_count += 1
                _LOGGER.info(u'%s %s -> term_bet: %s, term_win: %s, rate: %s%%, try count: %s' %
                             (tag, number, bet_this_term, win_this_term, round(new_profit_rate * 100, 3), try_count))
                if not need_adjust_number(lottery_type, new_win, new_bet_count, ignore_apply_rate=True):
                    final_number = number
                    _LOGGER.info(u'%s adjust succ, update number %s' % (tag, final_number))
                    break
            if not final_number:
                final_number = choose_user_lose_number(candidate_dict, preset.preset_min_profit_rate)
                _LOGGER.info(u'%s adjust fail, choose_user_lose_number %s' % (tag, final_number))
        else:
            final_number = number
    else:
        _LOGGER.info('%s no user bet, skip adjust' % tag)
        final_number = normal_random(lottery_type)

    result = calc_win(lottery_type, final_number, orders)
    win_this_term = sum(x['win_amount'] for x in result)
    new_profit_rate = (system_win + bet_this_term - win_this_term) / (total_bet + bet_this_term) \
        if (total_bet + bet_this_term) > 0 else 0
    _LOGGER.info('%s final number <%s>, bet: %s, win: %s, rate: %s%%' %
                 (tag, final_number, bet_this_term, win_this_term, round(new_profit_rate * 100, 3)))
    if new_profit_rate < 0:
        _LOGGER.warn('!!!!!!!!!!!!!%s current profit rate %s lt 0 !!!!!!!', tag, round(new_profit_rate * 100, 3))
    activity = sql_wrapper(fullfill_result(lottery_type, term, final_number, None))
    if activity:
        TimerEvent.submit(TIMER_EVENT_TYPE.CALC_STATS,
                          {'number': final_number, 'term': term, 'activity_type': lottery_type},
                          tz.now_ts())
        TimerEvent.submit(TIMER_EVENT_TYPE.ACTIVITY_ANNOUNCE,
                          {'number': final_number, 'term': term, 'activity_type': lottery_type},
                          tz.now_ts())
